//
//  Project.swift
//  JKTool
//
//  Created by 姜奎 on 2020/5/29.
//  Copyright © 2020 JK. All rights reserved.
//

import Foundation
import AppKit
import PathKit
import XcodeProj

enum BuildType: CustomStringConvertible {
    var description: String {
        return "\(self.libName()).\(self.ext())"
    }
    
    case Static(name: String,libName:String,dstPath: String)
    case Framework(name: String,libName:String)
    case Bundle(name: String,libName:String)
    
    func isStatic() -> Bool {
        switch self {
        case .Static(_,_,_):
            return true
        case .Framework(_,_):
            return false
        case .Bundle(_,_):
            return false
        }
    }
    
    func isFramework() -> Bool {
        switch self {
        case .Static(_,_,_):
            return false
        case .Framework(_,_):
            return true
        case .Bundle(_,_):
            return false
        }
    }
    
    func isBundle() -> Bool {
        switch self {
        case .Static(_,_,_):
            return false
        case .Framework(_,_):
            return false
        case .Bundle(_,_):
            return true
        }
    }
    
    func name() -> String {
        switch self {
        case .Static(let name,_,_):
            return name
        case .Framework(let name,_):
            return name
        case .Bundle(let name,_):
            return name
        }
    }
    
    func libName() -> String {
        switch self {
        case .Static(_,let libName,_):
            return libName
        case .Framework(_,let libName):
            return libName
        case .Bundle(_,let libName):
            return libName
        }
    }
    
    func ext(_ isXCFramework: Bool? = false) -> String {
        switch self {
        case .Static(_,_,_):
            return "a"
        case .Framework(_,_):
            return isXCFramework == true ? "xcframework":"framework"
        case .Bundle(_,_):
            return "bundle"
        }
    }
    
    func dstPath() -> String {
        switch self {
        case .Static(_,_,let dstPath)://"Copy (.*?)\\.h"
            return dstPath
        case .Framework(_,_):
            return ""
        case .Bundle(_,_):
            return ""
        }
    }
    
    func libBuildPath(_ configuration: String, sdk: String,sdkType: SdkType) -> String {
        return "\(Platform(sdk).libBuildPath(configuration, sdk: sdkType))"
    }
    
}

enum ProjectType {
    case xcodeproj(entrance: String)
    case workspace(entrance: String)
    case other(entrance: String)

    func entrance() -> String {
        switch self {
            case .xcodeproj(let entrance):
                return entrance
            case .workspace(let entrance):
                return entrance
            case .other(let entrance):
                return entrance
        }
    }

    func isWorkSpace() -> Bool {
        switch self {
            case .xcodeproj(_):
                return false
            case .workspace(_):
                return true
            case .other(_):
                return false
        }
    }

}

class Project {

    let directoryPath: String

        // 工程所在文件夹入口名称
    lazy var directoryEntrance: String = {
        return Path(directoryPath).lastComponent
    }()

    lazy var xcodeprojPath: Path? = {
        guard let xcodeprojPath = Path(self.directoryPath).glob("*.xcodeproj").first(where: { !$0.lastComponent.hasSuffix("-Debugging.xcodeproj") }) else {
            return nil
        }
        return xcodeprojPath.normalize()
    }()

    lazy var xcodeProj: XcodeProj? = {

        guard let xcodeprojPath = self.xcodeprojPath else {
            return nil
        }
        return try? XcodeProj(path: xcodeprojPath)
    }()

    func vaild() -> Bool {
        return xcodeProj != nil
    }

        // xcodeproj文件入口名称
    lazy var xcodeprojEntrance: String? = {
        guard let xcodeprojPath = self.xcodeprojPath else {
            return nil
        }

        return xcodeprojPath.lastComponent
    }()

        // 工程名称
    lazy var xcodeprojName: String? = {

        guard let entrance = xcodeprojEntrance else { return nil }

        return String(entrance.prefix(while: { $0 != "." }))
    }()

    lazy var debuggingXcodeprojPath: Path? = {

        if let xcodeprojPath = xcodeprojPath, let entrance = xcodeprojEntrance, let xcodeprojName = xcodeprojName {

            let newEntrance = String(entrance.replacingOccurrences(of: xcodeprojName, with: xcodeprojName + "-Debugging"))

            return Path(self.directoryPath) + newEntrance
        }

        return nil
    }()

    lazy var debuggingXcodeProj: XcodeProj? = {

        guard let debuggingXcodeprojPath = self.debuggingXcodeprojPath else {
            return nil
        }
        return try? XcodeProj(path: debuggingXcodeprojPath)
    }()

        // xcodeproj文件入口名称
    lazy var debuggingXcodeprojEntrance: String? = {
        guard let debuggingXcodeprojPath = self.debuggingXcodeprojPath else {
            return nil
        }

        return debuggingXcodeprojPath.lastComponent
    }()

    // 仅用于工程外层workspase
    lazy var workSpacePath: Path? = {
        guard let xcodeprojPath = Path(self.directoryPath).glob("*.xcworkspace").first(where: {!$0.lastComponent.hasSuffix("-Debugging.xcworkspace")}) else {
            return nil
        }
        return xcodeprojPath.normalize()
    }()

        // 仅用于工程外层workspase
    lazy var workspace: XCWorkspace? = {
        guard let workSpacePath = self.workSpacePath else {
            return nil
        }
        return try? XCWorkspace(path: workSpacePath)
    }()

        // workSpace文件入口名称
    lazy var workSpaceEntrance: String? = {
        guard let workSpacePath = self.workSpacePath else {
            return nil
        }

        return workSpacePath.lastComponent
    }()

        // 工程名称
    lazy var workSpaceName: String? = {

        guard let entrance = workSpaceEntrance else { return nil }

        return String(entrance.prefix(while: { $0 != "." }))
    }()


    // 仅用于工程外层workspase
    lazy var debuggingWorkSpacePath: Path? = {
        if let workSpacePath = workSpacePath, let entrance = workSpaceEntrance, let workSpaceName = workSpaceName {

            let newEntrance = String(entrance.replacingOccurrences(of: workSpaceName, with: workSpaceName + "-Debugging"))

            return Path(self.directoryPath) + newEntrance
        }

        return nil
    }()

        // 仅用于工程外层workspase
    lazy var debuggingWorkspace: XCWorkspace? = {
        guard let debuggingWorkSpacePath = self.debuggingWorkSpacePath else {
            return nil
        }
        return try? XCWorkspace(path: debuggingWorkSpacePath)
    }()

        // workSpace文件入口名称
    lazy var debuggingWorkSpaceEntrance: String? = {
        guard let debuggingWorkSpacePath = self.debuggingWorkSpacePath else {
            return nil
        }

        return debuggingWorkSpacePath.lastComponent
    }()

    lazy var projectType: ProjectType = {
        if let workSpaceEntrance = self.workSpaceEntrance  {
            return .workspace(entrance: workSpaceEntrance)
        }
        if let xcodeprojEntrance = self.xcodeprojEntrance  {
            return .xcodeproj(entrance: xcodeprojEntrance)
        }
        return .other(entrance: directoryEntrance)
    }()

        // 工程入口名称
    lazy var debugXcodeEntrance: String? = {

        if let debuggingWorkSpaceEntrance = self.debuggingWorkSpaceEntrance  {
            return debuggingWorkSpaceEntrance
        }

        if let workSpaceEntrance = self.workSpaceEntrance  {
            return workSpaceEntrance
        }

        guard let debuggingXcodeprojEntrance = self.debuggingXcodeprojEntrance else {
            return debuggingXcodeprojEntrance
        }

        return xcodeprojEntrance
    }()
    
    // 工程入口名称
    lazy var xcodeEntrance: String? = {

        if let workSpaceEntrance = self.workSpaceEntrance  {
            return workSpaceEntrance
        }

        return xcodeprojEntrance
    }()


    lazy var buildTypes: [BuildType]? = {

        let libProductTypes: [PBXProductType] = [.framework,.staticFramework,.dynamicLibrary,.staticLibrary, .bundle]

        let nativeTargets = self.xcodeProj?.pbxproj.nativeTargets.compactMap { target in
            return target.buildType
        }

        return nativeTargets
    }()
    
    lazy var defaultTarget: String = {

        let productTypes: [PBXProductType] = [.framework,.staticFramework,.dynamicLibrary,.staticLibrary, .application]

        let name = self.xcodeProj?.pbxproj.nativeTargets.compactMap { target in

            if productTypes.contains(target.productType ?? .none) {
                return target.name
            }
            return nil
        }.first

        return name ?? self.directoryEntrance
    }()
    
    lazy var modulefilePath: String = {
        return self.directoryPath.appending("/Modulefile")
    }()
    
    lazy var modulefile: String = {
        do {
            return try String(contentsOf: URL(fileURLWithPath: self.modulefilePath))
        } catch {
            return ""
        }
    }()
    
    lazy var moduleList:[SubProject] = {
        var list:[SubProject] = []
        self.modulefile.enumerateLines { (line, stop) in
            let scannerWithComments = Scanner(string: line)

            if scannerWithComments.scanString("#") != nil {
                return
            }
            if scannerWithComments.isAtEnd {
                // The line was all whitespace.
                return
            }
            
            var remainingString = scannerWithComments.string.replacingOccurrences(of: "\"", with: "")
            
            remainingString = remainingString.replacingOccurrences(of: "\'", with: "")
            
            remainingString = remainingString.replacingOccurrences(of: "\\", with: "")
            
            var arr = remainingString.components(separatedBy: " ").filter { (str) -> Bool in
                return str != ""
            }
            if arr.count <= 2 {
                return
            }
            
            let module = SubProject(name: arr[0], url: arr[1], branch: (arr.count >= 3) ? (arr[2]) : nil )
            list.append(module)
        }
        return list
    }()
    
    lazy var recordListPath: String = {
        return self.directoryPath.appending("/Modulefile.recordList")
    }()
    
    lazy var recordList: [String] = {
        do {
            let data = try Data(contentsOf: URL(fileURLWithPath: self.recordListPath))
            let recordList = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as! Array<String>
            return recordList
        } catch {
            return []
        }
    }()
    
    lazy var gitSubmodules: [String] = {
        
        guard let gitmodulesData = FileManager.default.contents(atPath: ".gitmodules"),
              let gitmodulesString = String(data: gitmodulesData, encoding: .utf8),
              let regex = try? NSRegularExpression(pattern: #"path\s*=\s*(.+)"#)
        else {
            return [String]()
        }

        // 使用正则表达式匹配子模块路径
        let matches = regex.matches(in: gitmodulesString, range: NSRange(gitmodulesString.startIndex..., in: gitmodulesString))

        // 提取子模块名称
        let submoduleNames = matches.map { match in
            (gitmodulesString as NSString).substring(with: match.range(at: 1))
        }
        return submoduleNames
        
    }()
    
    lazy var buildSimulatorLogPath: String = {
        return self.buildPath.appending("/build-Simulator.log")
    }()
    
    lazy var buildRealMachineLogPath: String = {
        return self.buildPath.appending("/build-RealMachine.log")
    }()
    
    lazy var buildBundleLogPath: String = {
        return self.buildPath.appending("/build-Bundle.log")
    }()
    lazy var buildScriptPath: String = {
        return self.directoryPath.appending("/build.sh")
    }()
    
    lazy var checkoutsPath: String = {
        let checkoutsPath = self.directoryPath.appending("/\(ModulesConfigs.sharedInstance.config.checkouts)")
        return checkoutsPath
    }()
    
    lazy var buildsPath: String = {
        let buildPath = self.directoryPath.appending("/\(ModulesConfigs.sharedInstance.config.builds)")
        return buildPath
    }()
    
    lazy var buildPath: String = {
        let buildPath = self.directoryPath.appending("/\(ModulesConfigs.sharedInstance.config.build)")
        return buildPath
    }()

    lazy var rootProject: Project = {
        guard let range = self.directoryPath.range(of: "/\(ModulesConfigs.sharedInstance.config.checkouts)") else {
            return self
        }
        
        let rootPath = String(self.directoryPath[..<range.lowerBound])
        return Project.project(directoryPath: rootPath)!
    }()

    lazy var recordTree: [[String]] = {
        var tree: [String:[String]] = [:]
        for record in self.recordList {
            guard let pro = Project.project(directoryPath: "\(self.checkoutsPath)/\(record)") else {
                continue
            }
            tree[record] = pro.recordList
        }

        var priorityModules: [[String]] = []
        func buildPriority(source: [String:[String]], target: Set<String>) {
            var priorityModule = [String]()

            var buildSource = source.filter { (key, value) in
                if Set(value).isSubset(of: target) {
                    priorityModule.append(key)
                    return false
                }
                return true
            }
            priorityModules.append(priorityModule)
            if buildSource.isEmpty {
                return
            }
            buildPriority(source: buildSource, target: Set(priorityModules.flatMap({ $0 })))
        }
        buildPriority(source: tree, target: Set(priorityModules.flatMap({ $0 })))
        return priorityModules
    }()
    
    init(directoryPath: String) {
        self.directoryPath = directoryPath
    }

}

extension Project {
    
    /// 写入RecordList
    /// - Parameter recordList: 需要写入的List
    /// - Returns: 返回剔除的旧item
    func writeRecordList(recordList: Array<String>) throws -> [String] {
        // 检查是否还有SubProject。没有则直接return
        // 写入当前工程所有SubProject
        let oldRecordList = self.recordList
        
        if recordList.isEmpty {
            if FileManager.default.fileExists(atPath: self.recordListPath) {
                try? FileManager.default.removeItem(atPath: self.recordListPath)
            }
            return []
        }

        if oldRecordList.isEmpty || !recordList.elementsEqual(oldRecordList)  {
            try recordList.dataValue().write(to: self.recordListPath)
            return oldRecordList.compactMap { record in
                if !recordList.contains(record) {
                    return record
                }
                return nil
            }
        }
        return []
    }
    
    func writeBuildSimulatorLog(log: String) {
        let data = log.data(using: .utf8)
        _ = try? data?.write(to: URL(fileURLWithPath: self.buildSimulatorLogPath), options: .atomicWrite)
    }
    
    func writeBuildRealMachineLog(log: String) {
        let data = log.data(using: .utf8)
        _ = try? data?.write(to: URL(fileURLWithPath: self.buildRealMachineLogPath), options: .atomicWrite)
    }
    
    func writeBuildBundleLog(log: String) {
        let data = log.data(using: .utf8)
        _ = try? data?.write(to: URL(fileURLWithPath: self.buildBundleLogPath), options: .atomicWrite)
    }
    
    func writeBuildLog(log: String, target: BuildType, isSimulator: Bool) -> String {
        let data = log.data(using: .utf8)
        let path: String
        switch target {
        case .Static(_, _, _),.Framework(_, _):
            path = isSimulator ? self.buildSimulatorLogPath:self.buildRealMachineLogPath
            _ = try? data?.write(to: URL(fileURLWithPath: path), options: .atomicWrite)
            return path
        case .Bundle(_, _):
            path = self.buildRealMachineLogPath
            _ = try? data?.write(to: URL(fileURLWithPath: path), options: .atomicWrite)
            return path
        }
        
    }
    
    func writeArchiveLog(log: String, scheme: String, configuration: String) -> String {
        let data = log.data(using: .utf8)
        let path = self.buildPath.appending("/\(scheme)-\(configuration)-archive.log")
        _ = try? data?.write(to: URL(fileURLWithPath: path), options: .atomicWrite)
        return path
    }
    
    func writeExportLog(log: String, scheme: String, configuration: String) -> String {
        let data = log.data(using: .utf8)
        let path = self.buildPath.appending("/\(scheme)-\(configuration)-export.log")
        _ = try? data?.write(to: URL(fileURLWithPath: path), options: .atomicWrite)
        return path
    }
    
    static func project(directoryPath: String = FileManager.default.currentDirectoryPath) -> Project? {

        let exist = FileManager.default.fileExists(atPath: directoryPath)
        if !exist {
            return nil
        }
        return Project(directoryPath: directoryPath.convertRelativePath())
    }
}

extension Project: Equatable {
    public static func == (lhs: Project, rhs: Project) -> Bool {
        return lhs.directoryPath == rhs.directoryPath
    }
    
    public static func != (lhs: Project, rhs: Project) -> Bool {
        return lhs.directoryPath != rhs.directoryPath
    }
}

extension Array where Element: Hashable {
    func filterDuplicates() -> [Element] {
        var seen = Set<Element>()
        return filter { seen.insert($0).inserted }
    }
}


extension PBXNativeTarget {
//let libProductTypes: [PBXProductType] = [.framework,.staticFramework,.dynamicLibrary,.staticLibrary, .bundle]
    var buildType: BuildType? {
        switch productType {
            case .framework, .staticFramework, .xcFramework:
                return BuildType.Framework(name: self.name, libName: String((self.product?.path ?? "").prefix(while: { $0 != "." })))
            case .staticLibrary, .dynamicLibrary:

                let dstPath = (self.buildPhases.first(where: { $0 is PBXCopyFilesBuildPhase }) as? PBXCopyFilesBuildPhase)?.dstPath

                return BuildType.Static(name: self.name, libName: String((self.product?.path ?? "").prefix(while: { $0 != "." })), dstPath: dstPath ?? "")

            case .bundle:
                return BuildType.Bundle(name: self.name, libName: String((self.product?.path ?? "").prefix(while: { $0 != "." })))
            default:
                return nil
        }
    }

}
